# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import math

_CORRUPTION_POSITIVE = (1. + 1.e-10)
_CORRUPTION_NEGATIVE = (1. - 1.e-10)


class TickPrice(object):
  def get_next_price(self, price, n_ticks=1):
    raise NotImplementedError()

  def get_prev_price(self, price, n_ticks=1):
    raise NotImplementedError()

  def ceil(self, price):
    raise NotImplementedError()

  def floor(self, price):
    raise NotImplementedError()

  def round(self, price):
    raise NotImplementedError()

  def format(self, price):
    raise NotImplementedError()

  @property
  def max_precision(self):
    raise NotImplementedError()


class FixedTickPrice(TickPrice):
  def __init__(self, tick_size, *, precision=None):
    self.tick_size = float(tick_size)
    if precision is not None:
      self._precision = precision
    else:
      self._precision = FixedTickPrice._calculate_precision(tick_size)
    assert int(self._precision) == self._precision

  @staticmethod
  def _calculate_precision(tick_size):
    precision = int(-math.floor(math.log10(tick_size + 1.e-10)))
    if precision < 0:
      return 0
    else:
      return precision

  def get_next_price(self, price, n_ticks=1):
    # Assumes price is valid price. Because of floating point error, it is hard
    # to implement is_valid_price function.
    return price + n_ticks * self.tick_size

  def get_prev_price(self, price, n_ticks=1):
    return price - n_ticks * self.tick_size

  def ceil(self, price):
    return math.ceil(price / self.tick_size) * self.tick_size

  def floor(self, price):
    return math.floor(price / self.tick_size) * self.tick_size

  def round(self, price):
    # Note: https://docs.python.org/3/tutorial/floatingpoint.html
    return round(price / self.tick_size) * self.tick_size

  def format(self, price):
    return ('%%.%df' % self._precision) % self.round(price)

  @property
  def max_precision(self):
    return self._precision


class RangeVariableTickPrice(TickPrice):
  def _get_next_tick(self, price):
    return self._get_tick(price * _CORRUPTION_POSITIVE)

  def _get_prev_tick(self, price):
    return self._get_tick(price * _CORRUPTION_NEGATIVE)

  def get_next_price(self, price):
    floored = self.floor(price)
    tick_price = self._get_next_tick(floored)
    return self.floor(floored + tick_price)

  def get_prev_price(self, price):
    ceiled = self.ceil(price)
    tick_price = self._get_prev_tick(price)
    return self.ceil(ceiled - tick_price)

  def floor(self, price):
    corrupted_price = price * _CORRUPTION_POSITIVE
    tick_price = self._get_prev_tick(corrupted_price)
    return math.floor(corrupted_price / tick_price) * tick_price

  def ceil(self, price):
    corrupted_price = price * _CORRUPTION_NEGATIVE
    tick_price = self._get_next_tick(corrupted_price)
    return math.ceil(corrupted_price / tick_price) * tick_price

  def round(self, price):
    floored = self.floor(price)
    ceiled = self.ceil(price)
    assert price >= floored
    assert price <= ceiled
    if price - floored < ceiled - price:
      return floored
    else:
      return ceiled

  def format(self, price):
    rounded = self.round(price)
    if rounded >= 100.:
      return '%d' % int(rounded)
    elif rounded >= 10.:
      return '%.1f' % rounded
    else:
      return '%.2f' % rounded


# Upbit KRW
class UpbitKrwTickPrice(RangeVariableTickPrice):
  def _get_tick(self, price_corrected):
    if price_corrected >= 2000000.:
      return 1000.
    elif price_corrected >= 1000000.:
      return 500.
    elif price_corrected >= 500000.:
      return 100.
    elif price_corrected >= 100000.:
      return 50.
    elif price_corrected >= 10000.:
      return 10.
    elif price_corrected >= 1000.:
      return 5.
    elif price_corrected >= 100.:
      return 1.
    elif price_corrected >= 10.:
      return 0.1
    else:
      return 0.01

  @property
  def max_precision(self):
    return 2


# Bithumb KRW
class BithumbKrwTickPrice(RangeVariableTickPrice):
  def _get_tick(self, price_corrected):
    if price_corrected >= 1000000.:
      return 1000.
    elif price_corrected >= 500000.:
      return 500.
    elif price_corrected >= 100000.:
      return 100.
    elif price_corrected >= 50000.:
      return 50.
    elif price_corrected >= 10000.:
      return 10.
    elif price_corrected >= 5000.:
      return 5.
    elif price_corrected >= 100.:
      return 1.
    elif price_corrected >= 10.:
      return 0.1
    else:
      return 0.01

  @property
  def max_precision(self):
    return 2


# Coinone KRW as of 20200507
class CoinoneKrwTickPrice(RangeVariableTickPrice):
  def _get_tick(self, price_corrected):
    if price_corrected >= 1000000.:
      return 1000.
    elif price_corrected >= 500000.:
      return 500.
    elif price_corrected >= 100000.:
      return 100.
    elif price_corrected >= 50000.:
      return 50.
    elif price_corrected >= 10000.:
      return 10.
    elif price_corrected >= 5000.:
      return 5.
    elif price_corrected >= 1000.:
      return 1.
    elif price_corrected >= 100.:
      return 0.1
    elif price_corrected >= 10.:
      return 0.01
    elif price_corrected >= 1.:
      return 0.001
    else:
      return 0.0001

  @property
  def max_precision(self):
    return 2


class GopaxKrwTickPrice(RangeVariableTickPrice):
  def _get_tick(self, price_corrected):
    if price_corrected >= 100000000.:
      return 100000.
    elif price_corrected >= 50000000.:
      return 50000.
    elif price_corrected >= 10000000.:
      return 10000.
    elif price_corrected >= 5000000.:
      return 5000.
    elif price_corrected >= 1000000.:
      return 1000.
    elif price_corrected >= 500000.:
      return 500.
    elif price_corrected >= 100000.:
      return 100.
    elif price_corrected >= 50000.:
      return 50.
    elif price_corrected >= 10000.:
      return 10.
    elif price_corrected >= 5000.:
      return 5.
    elif price_corrected >= 1000.:
      return 1.
    elif price_corrected >= 500.:
      return 0.5
    elif price_corrected >= 100.:
      return 0.1
    elif price_corrected >= 50.:
      return 0.05
    elif price_corrected >= 10.:
      return 0.01
    elif price_corrected >= 5.:
      return 0.005
    elif price_corrected >= 1.:
      return 0.001
    elif price_corrected >= 0.5:
      return 0.0005
    else:
      return 0.0001

  @property
  def max_precision(self):
    return 4


# Bitfinex
class BitfinexTickPrice(TickPrice):
  SIG_DIGITS_BASE = 4

  def get_next_price(self, price):
    floored = self.floor(price)
    sig_digits = math.floor(math.log10(floored * _CORRUPTION_POSITIVE))
    tick_price = 10**(sig_digits - self.SIG_DIGITS_BASE)
    return self.floor(floored + tick_price)

  def get_prev_price(self, price):
    ceiled = self.ceil(price)
    sig_digits = math.floor(math.log10(ceiled * _CORRUPTION_NEGATIVE))
    tick_price = 10**(sig_digits - self.SIG_DIGITS_BASE)
    return self.ceil(ceiled - tick_price)

  def ceil(self, price):
    corrupted_price = price * _CORRUPTION_NEGATIVE
    sig_digits = math.floor(math.log10(corrupted_price))
    tick_price = 10**(sig_digits - self.SIG_DIGITS_BASE)
    return math.ceil(corrupted_price / tick_price) * tick_price

  def floor(self, price):
    corrupted_price = price * _CORRUPTION_POSITIVE
    sig_digits = math.floor(math.log10(corrupted_price))
    tick_price = 10**(sig_digits - self.SIG_DIGITS_BASE)
    return math.floor(corrupted_price / tick_price) * tick_price

  def round(self, price):
    sig_digits = math.floor(math.log10(price))
    tick_price = 10**(sig_digits - self.SIG_DIGITS_BASE)
    return round(price / tick_price) * tick_price

  def format(self, price):
    sig_digits = math.floor(math.log10(price))
    return ('%%.%df' % -(sig_digits - self.SIG_DIGITS_BASE)) % price

  @property
  def max_precision(self):
    return 8


class BitfinexBTCUSDTickPrice(TickPrice):
  def get_next_price(self, price, n_ticks=1):
    # Assumes price is valid price. Because of floating point error, it is hard
    # to implement is_valid_price function.
    assert n_ticks == 1
    if price >= 10000.:
      return price + 1.
    else:
      return price + 0.1

  def get_prev_price(self, price, n_ticks=1):
    # Assumes price is valid price. Because of floating point error, it is hard
    # to implement is_valid_price function.
    assert n_ticks == 1
    if price >= 10001.:
      return price - 1.
    else:
      return price - 0.1

  def ceil(self, price):
    if price >= 10000.:
      return math.ceil(price)
    else:
      return math.ceil(price / 0.1) * 0.1

  def floor(self, price):
    if price >= 10000.:
      return math.floor(price)
    else:
      return math.floor(price / 0.1) * 0.1

  def round(self, price):
    if price >= 10000.:
      return round(price)
    else:
      return round(price / 0.1) * 0.1

  def format(self, price):
    raise NotImplementedError()

  @property
  def max_precision(self):
    return 1
